﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Async;
using System.Web.Routing;
using System.Web.Script.Serialization;

namespace MvcPipe
{
    internal sealed class PipeletActionInvoker
    {
        internal const string CssReferencesHttpContextItemName          = "_MvcPipeCssReferences";
        internal const string JavascriptReferencesHttpContextItemName   = "_MvcPipeJavascriptReferences";
        internal const string JavascriptMvcCallbackFunction             = "MvcPipe.ReceivePipelet";

        private static readonly FieldInfo httpContextField = typeof(HttpContextWrapper).GetField("_context", BindingFlags.Instance | BindingFlags.NonPublic);

        internal Task InvokePipeletsAsync( HttpContextBase httpContext,IEnumerable<PipeletDescription> pipelets )
        {
            Contract.Requires<ArgumentNullException>( httpContext != null,"httpContext");
            Contract.Requires<ArgumentNullException>( pipelets != null,"pipelets");

            var sharedLock = new object();

            var pipeletTasks = pipelets.Select(p => InvokePipeletAsync(httpContext, p, sharedLock)).ToArray();

            return Task.Factory.ContinueWhenAll(pipeletTasks, resultTasks =>
            {
                var exceptions = resultTasks.Select(t => t.Exception).Where(e => e != null).ToArray();

                if ( exceptions.Any() )
                {
                    throw new AggregateException(exceptions);
                }
            });
        }

        private Task InvokePipeletAsync( HttpContextBase httpContext,PipeletDescription pipeletDescription,object sharedLock )
        {
            return InvokePipeletAsyncCore(httpContext, pipeletDescription).ContinueWith(resultTask =>
            {
                if (resultTask.Exception != null)
                {
                    throw resultTask.Exception;
                }

                var pipeletResult       = resultTask.Result;
                var serializationData   = new
                {
                    pipeletResult.Content,
                    pipeletResult.CssReferences,
                    pipeletResult.JavascriptReferences,
                    pipeletResult.Description.Name
                };

                var serializedPipelet = new JavaScriptSerializer().Serialize(serializationData);
                var payloadText = "<script type=\"text/javascript\">" + JavascriptMvcCallbackFunction + "(" + serializedPipelet + ");</script>";

                // Pad to force buffer flush:
                payloadText = PadStringTo(payloadText, 1024);

                lock (sharedLock)
                {
                    try
                    {
                        httpContext.Response.Write(payloadText);
                        httpContext.Response.Flush();
                    }
                    catch( HttpException )
                    {}
                }
            });
        }

        private Task<PipeletActionResult> InvokePipeletAsyncCore(HttpContextBase httpContext,PipeletDescription pipeletDescription)
        {
            pipeletDescription.Action = pipeletDescription.Action ?? pipeletDescription.Name;

            if ( string.IsNullOrWhiteSpace(pipeletDescription.Action))
            {
                throw new ArgumentException("No action specified for pipelet: " + pipeletDescription.Name);
            }

            var controllerName  = pipeletDescription.Controller ?? httpContext.Request.RequestContext.RouteData.GetRequiredString("controller");
            var routeValues     = pipeletDescription.RouteValues ?? new RouteValueDictionary();

            var routeData                   = new RouteData();
            routeData.Values["controller"]  = controllerName;
            routeData.Values["action"]      = pipeletDescription.Action;

            foreach (var kv in routeValues)
            {
                routeData.Values[kv.Key] = kv.Value;
            }

            var underlyingRealHttpContext = httpContextField.GetValue(httpContext) as HttpContext;

            if (underlyingRealHttpContext == null)
            {
                throw new InvalidOperationException("Rendering an action to a string is only possible from a real http request, not a child action");
            }

            var responseWriter      = new StringWriter();
            var innerContext        = new HttpContext(underlyingRealHttpContext.Request, new HttpResponse(responseWriter));

            var contextWrapper      = new HttpContextWrapper(innerContext);
            var requestContext      = new RequestContext(contextWrapper, routeData);

            var controllerFactory   = ControllerBuilder.Current.GetControllerFactory();
            var controller          = controllerFactory.CreateController(requestContext, controllerName);

            var result = new PipeletActionResult()
            {
                Description = pipeletDescription
            };

            if (controller != null)
            {
                var executionTask = ExecuteActionAsync(controller,contextWrapper, routeData, pipeletDescription.ForceAsync);
                return executionTask.ContinueWith(resultTask =>
                {
                    if ( resultTask.Exception != null )
                    {
                        throw resultTask.Exception;
                    }

                    responseWriter.Flush();
                    result.Content = responseWriter.GetStringBuilder().ToString();
                    responseWriter.Dispose();

                    result.CssReferences          = contextWrapper.Items[CssReferencesHttpContextItemName] as IEnumerable<PipeletCssReference> ?? new List<PipeletCssReference>();
                    result.JavascriptReferences   = contextWrapper.Items[JavascriptReferencesHttpContextItemName] as IEnumerable<string> ?? new List<string>();

                    controllerFactory.ReleaseController(controller);
                    return result;
                });
            }

            var completionSource = new TaskCompletionSource<PipeletActionResult>();
            completionSource.SetException(new InvalidOperationException("Unable to find controller: " + controllerName));
            return completionSource.Task;
        }

        private Task ExecuteActionAsync(IController controller, HttpContextBase httpContext, RouteData routeData, bool forceAsync)
        {
            var asyncController = controller as IAsyncController;
            return asyncController != null ? ExecuteActionAsyncCore(asyncController, httpContext, routeData) : ExecuteActionAsyncCore(controller, httpContext, routeData, forceAsync);
        }

        private Task ExecuteActionAsyncCore(IController controller, HttpContextBase httpContext, RouteData routeData, bool forceAsync)
        {
            var requestContext = new RequestContext(httpContext, routeData);

            if (forceAsync)
            {
                return Task.Factory.StartNew(() => controller.Execute(requestContext));
            }

            var completionSource = new TaskCompletionSource<int>();

            try
            {
                controller.Execute(requestContext);
                completionSource.SetResult(0);
            }
            catch (Exception ex)
            {
                completionSource.SetException(ex);
            }

            return completionSource.Task.ContinueWith(t => { }, TaskContinuationOptions.ExecuteSynchronously);
        }

        private Task ExecuteActionAsyncCore(IAsyncController controller, HttpContextBase httpContext, RouteData routeData)
        {
            var requestContext = new RequestContext(httpContext, routeData);

            var completionSource = new TaskCompletionSource<int>();
            try
            {
                controller.BeginExecute(requestContext, result =>
                {
                    try
                    {
                        controller.EndExecute(result);
                        completionSource.SetResult(0);
                    }
                    catch (Exception ex)
                    {
                        completionSource.SetException(ex);
                    }
                }, null);
            }
            catch (Exception ex)
            {
                completionSource.SetException(ex);
            }

            return completionSource.Task.ContinueWith(t => { }, TaskContinuationOptions.ExecuteSynchronously);
        }

        private static string PadStringTo( string input,int padWindowSize )
        {
            var byteCount = Encoding.UTF8.GetByteCount(input);
            var lastByteCount = byteCount%padWindowSize;

            var padBuilder = new StringBuilder();
            padBuilder.Append(input);

            for( int i = lastByteCount; i < padWindowSize; ++ i)
            {
                padBuilder.Append(' ');
            }

            return padBuilder.ToString();
        }
    }
}